<!DOCTYPE html>
<body>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="resources/gamepad-helpers.js"></script>
<script>

function changeOneGamepadId(gamepadIdString) {
    // Change the gamepad ID string to a new value.
    gamepadController.setId(0, gamepadIdString);
    gamepadController.dispatchConnected(0);
}

function changeTwoGamepadIds(gamepadIdString) {
    // Change the gamepad ID strings for two gamepads. Dispatch after modifying
    // both IDs to simulate both changing as a result of the same gamepad poll
    // event.
    gamepadController.setId(0, gamepadIdString);
    gamepadController.setId(1, gamepadIdString);
    gamepadController.dispatchConnected(0);
    gamepadController.dispatchConnected(1);
}

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    let connectListener1Called = false;
    let connectListener2Called = false;
    let disconnectListenerCalled = false;

    // Typically it is not possible for the gamepad connection state to change
    // while an event listener is being run, but the test API allows us to
    // disconnect a gamepad in javascript. Make sure that the gamepad state is
    // not changed while listeners are still running, and that disconnect
    // listeners are still called if the disconnect occurs within an event
    // listener.
    let connectListener1 = (e) => {
        assert_equals(
            e.gamepad.connected, true,
            "expected gamepad connected in first connect listener");
        disconnectGamepads();
        assert_equals(
            e.gamepad.connected, true,
            "expected gamepad still connected in first listener");
        connectListener1Called = true;
    };

    let connectListener2 = (e) => {
        assert_equals(
            e.gamepad.connected, true,
            "expected gamepad connected in second connect listener");
        disconnectGamepads();
        assert_equals(
            e.gamepad.connected, true,
            "expected gamepad still connected in second listener");
        connectListener2Called = true;
    };

    let disconnectListener = (e) => {
        assert_equals(
            e.gamepad.connected, false,
            "expected gamepad disconnected in disconnect listener");
        // Ensure all gamepadconnected listeners have completed.
        assert_equals(
            connectListener1Called, true,
            "expected first connect listener called before disconnect");
        assert_equals(
            connectListener2Called, true,
            "expected second connect listener called before disconnect");
        disconnectListenerCalled = true;
    };

    window.addEventListener('gamepadconnected', connectListener1);
    window.addEventListener('gamepadconnected', connectListener2);
    window.addEventListener('gamepaddisconnected', disconnectListener);

    // Simulate a connection, and expect a disconnection to occur during the
    // gamepadconnected event listeners.
    let connectPromise = ongamepadconnected();
    let disconnectPromise = ongamepaddisconnected();
    connectGamepads(1);
    await Promise.all([connectPromise, disconnectPromise]);

    assert_equals(connectListener1Called, true,
                  "expected first gamepadconnected listener to be called");
    assert_equals(connectListener2Called, true,
                  "expected second gamepadconnected listener to be called");
    assert_equals(disconnectListenerCalled, true,
                  "expected gamepaddisconnected listener to be called");

    testGamepadStateAllDisconnected();

    // Clean up.
    window.removeEventListener('gamepadconnected', connectListener1);
    window.removeEventListener('gamepadconnected', connectListener2);
    window.removeEventListener('gamepaddisconnected', disconnectListener);
}, "Gamepad disconnection inside event listener.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    let connectListener1Called = false;
    let connectListener2Called = false;

    let disconnectListener1Called = false;
    let disconnectListener2Called = false;

    // Configure two event listeners such that the first listener to be executed
    // will remove the other listener.
    let connectListener1 = (e) => {
        window.removeEventListener('gamepadconnected', connectListener2);
        connectListener1Called = true;
    };

    let connectListener2 = (e) => {
        window.removeEventListener('gamepadconnected', connectListener1);
        connectListener2Called = true;
    };

    let disconnectListener1 = (e) => {
        window.removeEventListener('gamepaddisconnected', disconnectListener2);
        disconnectListener1Called = true;
    };

    let disconnectListener2 = (e) => {
        window.removeEventListener('gamepaddisconnected', disconnectListener1);
        disconnectListener2Called = true;
    };

    window.addEventListener('gamepadconnected', connectListener1);
    window.addEventListener('gamepadconnected', connectListener2);
    window.addEventListener('gamepaddisconnected', disconnectListener1);
    window.addEventListener('gamepaddisconnected', disconnectListener2);

    // Simulate a connection.
    let connectPromise = ongamepadconnected();
    connectGamepads(1);
    await connectPromise;

    let numConnectListenersCalled = Number(connectListener1Called) +
                                    Number(connectListener2Called);
    assert_equals(numConnectListenersCalled, 1,
                  "expected exactly one connect listener to be called");

    // Simulate a disconnection.
    let disconnectPromise = ongamepaddisconnected();
    disconnectGamepads();
    await disconnectPromise;

    let numDisconnectListenersCalled = Number(disconnectListener1Called) +
                                       Number(disconnectListener2Called);
    assert_equals(numDisconnectListenersCalled, 1,
                  "expected exactly one disconnect listener to be called");

    testGamepadStateAllDisconnected();

    // Clean up.
    window.removeEventListener('gamepadconnected', connectListener1);
    window.removeEventListener('gamepadconnected', connectListener2);
    window.removeEventListener('gamepaddisconnected', disconnectListener1);
    window.removeEventListener('gamepaddisconnected', disconnectListener2);
}, "Remove event listener from inside event listener.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    // In normal circumstances, a gamepad is only connected or disconnected when
    // a signal is received from the host API. However, if the page is inactive
    // when this signal is received, the renderer checks for connection changes
    // and fires the appropriate events once the page is active.
    //
    // If both a disconnection and a connection occur while the page is
    // inactive, the renderer compares the previous device info with the current
    // device info to determine whether it is the same device. If it differs,
    // gamepaddisconnected and gamepadconnected are fired.
    //
    // This test simulates this case by changing the gamepad ID string.

    // Connect a gamepad.
    let connectPromise1 = ongamepadconnected();
    connectGamepads(1);
    await connectPromise1;

    // Register connect and disconnect listeners, and check that they are called
    // in the correct order (disconnect first).
    let connectListenerCalled = false;
    let disconnectListenerCalled = false;

    let disconnectListener = (e) => {
        assert_equals(connectListenerCalled, false,
                      "disconnect listener should be called first");
        assert_equals(disconnectListenerCalled, false,
                      "expected disconnect listener to be called only once");
        disconnectListenerCalled = true;
    };

    let connectListener = (e) => {
        assert_equals(disconnectListenerCalled, true,
                      "connect listener should be called second");
        assert_equals(connectListenerCalled, false,
                      "expected connect listener to be called only once");
        connectListenerCalled = true;
    };

    window.addEventListener('gamepadconnected', connectListener, {once: true});
    window.addEventListener('gamepaddisconnected', disconnectListener,
                            {once: true});

    // Simulate a change to the gamepad ID. This should be detected as a
    // different gamepad and fire connection change events.
    let disconnectPromise1 = ongamepaddisconnected();
    let connectPromise2 = ongamepadconnected();
    changeOneGamepadId("MockStick 3001");
    await Promise.all([disconnectPromise1, connectPromise2]);

    assert_equals(disconnectListenerCalled, true,
                  "expected disconnect listener to be called");
    assert_equals(connectListenerCalled, true,
                  "expected connect listener to be called");

    // Simulate a disconnection.
    let disconnectPromise2 = ongamepaddisconnected();
    disconnectGamepads();
    await disconnectPromise2;

    testGamepadStateAllDisconnected();
}, "Check disconnect listener called first during gamepad ID change.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    // Connect a gamepad.
    let connectPromise = ongamepadconnected();
    connectGamepads(1);
    await connectPromise;

    // Change the gamepad ID. When the ID is changed there are no
    // gamepadconnected listeners, but one is added in the gamepaddisconnected
    // listener. Make sure the newly-added gamepadconnected listener is called.
    let disconnectThenConnectPromise = new Promise(resolve => {
        window.addEventListener('gamepaddisconnected', (e) => {
            window.addEventListener('gamepadconnected', (e) => {
                resolve();
            }, {once: true});
        }, {once: true});
    });
    changeOneGamepadId("MockStick 3001");
    await disconnectThenConnectPromise;

    // Disconnect the gamepad.
    let disconnectPromise = ongamepaddisconnected();
    disconnectGamepads();
    await disconnectPromise;

    testGamepadStateAllDisconnected();
}, "Add connection event listener while handling a gamepad ID change.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    let eventWatcher = new EventWatcher(t, window, ['gamepadconnected',
                                                    'gamepaddisconnected']);
    let eventPromise = eventWatcher.wait_for([
        // Connect gamepads.
        'gamepadconnected',
        'gamepadconnected',
        // Change one gamepad ID.
        'gamepaddisconnected',
        'gamepadconnected',
        // Change the other gamepad ID.
        'gamepaddisconnected',
        'gamepadconnected',
        // Disconnect gamepads.
        'gamepaddisconnected',
        'gamepaddisconnected'
    ]);

    // Connect two gamepads.
    let connectPromise1 = onGamepadEventWithIndex('gamepadconnected', 0);
    let connectPromise2 = onGamepadEventWithIndex('gamepadconnected', 1);
    connectGamepads(2);
    await Promise.all([connectPromise1, connectPromise2]);

    // Simulate a gamepad ID change for both gamepads.
    let disconnectPromise1 = onGamepadEventWithIndex('gamepaddisconnected', 0);
    let disconnectPromise2 = onGamepadEventWithIndex('gamepaddisconnected', 1);
    let connectPromise3 = onGamepadEventWithIndex('gamepadconnected', 0);
    let connectPromise4 = onGamepadEventWithIndex('gamepadconnected', 1);
    changeTwoGamepadIds("MockStick 3001");
    await Promise.all([connectPromise3, connectPromise4,
                       disconnectPromise1, disconnectPromise2]);

    // Disconnect both gamepads.
    let disconnectPromise3 = onGamepadEventWithIndex('gamepaddisconnected', 0);
    let disconnectPromise4 = onGamepadEventWithIndex('gamepaddisconnected', 1);
    disconnectGamepads();
    await Promise.all([disconnectPromise3, disconnectPromise4]);

    // Verify that gamepadconnected and gamepaddisconnected events were received
    // in the expected order.
    await eventPromise;
    testGamepadStateAllDisconnected();
}, "Check connection event order during two gamepad ID changes.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    // Connect two gamepads.
    let connectPromise1 = onGamepadEventWithIndex('gamepadconnected', 0);
    let connectPromise2 = onGamepadEventWithIndex('gamepadconnected', 1);
    connectGamepads(2);
    await Promise.all([connectPromise1, connectPromise2]);

    // Change the gamepad IDs. When the ID is changed there are gamepadconnected
    // and gamepaddisconnected listeners, but both are removed by the
    // gamepaddisconnected listener. This leaves zero listeners for the
    // remainder of events. Make sure the removed listeners are not called.
    let listenersRemoved = false;
    let disconnectListener = (e) => {
        assert_equals(listenersRemoved, false,
                      "disconnect listener called unexpectedly");
        window.removeEventListener('gamepaddisconnected', disconnectListener);
        window.removeEventListener('gamepadconnected', connectListener);
        listenersRemoved = true;
    }
    let connectListener = (e) => {
        assert_equals(listenersRemoved, false,
                      "connect listener called unexpectedly");
    };
    window.addEventListener('gamepadconnected', connectListener);
    window.addEventListener('gamepaddisconnected', disconnectListener);

    // Simulate changing both gamepad IDs. Set a zero-length timeout instead of
    // waiting for connection events to avoid registering more listeners.
    changeTwoGamepadIds("MockStick 3001");
    await new Promise(resolve => setInterval(resolve, 0));

    // Disconnect both gamepads.
    let disconnectPromise1 = onGamepadEventWithIndex('gamepaddisconnected', 0);
    let disconnectPromise2 = onGamepadEventWithIndex('gamepaddisconnected', 1);
    disconnectGamepads();
    await Promise.all([disconnectPromise1, disconnectPromise2]);

    testGamepadStateAllDisconnected();
}, "Remove all connection event listeners while handling a gamepad ID change.");

promise_test(async (t) => {
    disconnectGamepads();
    testGamepadStateAllDisconnected();

    // Connect two gamepads.
    let connectPromise1 = onGamepadEventWithIndex('gamepadconnected', 0);
    let connectPromise2 = onGamepadEventWithIndex('gamepadconnected', 1);
    connectGamepads(2);
    await Promise.all([connectPromise1, connectPromise2]);

    // Ensure that the gamepad state is already updated inside the listener.
    let disconnectListener = () => {
        let gamepads = navigator.getGamepads();
        assert_equals(gamepads[0], null);
        assert_equals(gamepads[1], null);
    };
    window.addEventListener('gamepaddisconnected', disconnectListener);

    // Disconnect both gamepads.
    let disconnectPromise1 = onGamepadEventWithIndex('gamepaddisconnected', 0);
    let disconnectPromise2 = onGamepadEventWithIndex('gamepaddisconnected', 1);
    disconnectGamepads();
    await Promise.all([disconnectPromise1, disconnectPromise2]);

    window.removeEventListener('gamepaddisconnected', disconnectListener);
    testGamepadStateAllDisconnected();
}, "Query state inside multiple callbacks does not cause re-entrancy");

</script>
</body>
